Pythonの「Annotated」を初心者に向けてわかりやすく解説する
Pythonの「Annotated」に関して、意外と理解するのに時間がかかってしまったので、自分の理解のため兼(なるべく難しい言葉を使わないよう)初心者の方に向けてわかりやすくまとめます
結論を先に言うと、自分の中で腹落ちしたのは「コードで記述してカスタムすることができる型定義」という説明です。
これだけ聞いてもなんのことかわからないと思いますが、ここからわかりやすく解説していきます
目次
なぜ型を定義するのか
Annotatedは型ヒント、型定義をするためのものですが、そもそもなぜ型定義をするのかというと、よく一般的に言われているのは
- 間違いを早く見つけられる:コードを書いているときに、変な値を使おうとすると教えてくれる
- コードが読みやすくなる:他の人(未来の自分も含めて)がコードを読むとき、どんな種類の情報を扱っているか分かりやすい
あたりかなと思います、要するに読みやすくて保守がしやすい良いコードを書く上で重要になるということですね
Annotatedを使うには
「Annotated」はPython 3.9から追加されました、使用するには下記のライブラリも必要になります
- Pydantic:データの型を決めて、それが正しいかチェックしてくれるツール
- mypy:コードを実行する前に、型の使い方が正しいか確認してくれるツール
これらは通常のPythonには含まれていないので、別途インストールする必要があります
Annotatedの構文
Annotatedはこんな風に使います
Annotated[型, 追加情報1, 追加情報2, ...]
- 型(第一引数、必須):int(整数)やstr(文字列)など、通常のPythonの型
- 追加情報(第二引数以降、オプション):その型に付けたい特別なルールや説明、複数付けることも可
例えば
from typing import Annotated
from pydantic import Field
ValidScore = Annotated[int, Field(ge=0, le=100)]
これは「0以上100以下の整数」という型を作っています
Annotatedの使い方
実際のコードだとこんな感じです
第二引数で定めたルールを満たさない場合にValueErrorが発生していることがわかりますね
from typing import Annotated
from pydantic import Field, BaseModel
ValidScore = Annotated[int, Field(ge=0, le=100)]
class Student(BaseModel):
name: str
score: ValidScore
# ルール内(0以上100以下)
good_student = Student(name="Alice", score=85)
print(good_student) # 問題なく動く
# ルール外
try:
bad_student = Student(name="Bob", score=150) # ここでエラー発生
except ValueError as e:
print(f"エラーが起きました:{e}")
なぜAnnotatedを使うのか?
- 細かいルールを付けられる:「この数字は0以上100以下じゃないとダメ」といった細かい条件を、型の定義に直接書ける
- 分かりやすい:score: ValidScoreと書くだけでその数字に特別なルールがあることが一目で分かる
- 再利用できる:一度ValidScoreを定義すればいろんな場所で同じルールを簡単に使い回せる
- 間違いを防げる:不正な値を使おうとするとすぐにエラーを出してくれる
Pydanticやmypyが必要な理由
Annotatedですが、
from pydantic import Field, BaseModel
とある通りPydanticやmypyが必要になります
ちょっと無理やりですが、これらのライブラリ無しで使おうとすると下記のように条件を満たしてないのに普通に動いてしまいます
from typing import Annotated
ValidScore = Annotated[int, "0以上100以下の整数"]
def process_score(score: ValidScore):
print(f"スコア: {score}")
# これらが問題なく動いてしまう
process_score(150)
process_score("not a number")
では一体、Pydanticとmypyは具体的にどんな役割をしているのかと言うと
Pydanticの役割
Pydanticは、データの形が正しいかをチェックしてくれます。先ほどのStudentクラスを例に取ると
from typing import Annotated
from pydantic import BaseModel, Field
ValidScore = Annotated[int, Field(ge=0, le=100)]
class Student(BaseModel):
name: str
score: ValidScore
# Pydanticが働く場面
try:
student = Student(name="Alice", score=150) # ここでPydanticがチェックします
except ValueError as e:
print(f"エラー: {e}") # "エラー: 1 validation error for Student..."と出力されます
この例では、Studentクラスを作るときにPydanticが自動的にscoreの値をチェックし、150は100より大きいためPydanticがエラーを発生させてくれました
mypyの役割
mypyは、コードを実行する前に型の使い方が正しいか確認してくれます
例えば
from typing import Annotated
from pydantic import Field
ValidScore = Annotated[int, Field(ge=0, le=100)]
def process_score(score: ValidScore):
print(f"スコア: {score}")
# mypyがチェックする部分
process_score("not a number") # mypyはここで警告を出します
このコードを保存してmypyコマンドを実行すると、以下のような警告が表示されます:
error: Argument 1 to "process_score" has incompatible type "str"; expected "int"
mypyは、"not a number"という文字列をValidScore(つまり整数)として扱おうとしていることを教えてくれます
両者の組み合わせ
Pydanticとmypyを組み合わせることで、コードを書いているときの型チェック(mypy)と、プログラム実行時のデータチェック(Pydantic)の両方を行うことができるということになります
例えば
from typing import Annotated
from pydantic import BaseModel, Field
ValidScore = Annotated[int, Field(ge=0, le=100)]
class Student(BaseModel):
name: str
score: ValidScore
def process_student(student: Student):
print(f"{student.name}のスコア: {student.score}")
# mypyがチェック(コードを書いているとき)
process_student({"name": "Alice", "score": "invalid"}) # mypyが警告を出します
# Pydanticがチェック(プログラム実行時)
try:
student = Student(name="Bob", score=150) # Pydanticがエラーを出します
process_student(student)
except ValueError as e:
print(f"エラー: {e}")
このように、mypyはコードを書いている段階で型の使い方の間違いを指摘し、Pydanticは実際にプログラム実行時にデータの正しさをチェックします
これにより、Annotatedで定義した型とそのルールが、コードの書き方とデータの扱い方の両方で守られることになり、より安全で信頼性の高いコードを書くことができるのです
まとめ
ということで、Pythonの基本の型(str, int, float)などをベースに、条件をつけることで独自の型を定義することができるのが「Annotated」の使い方ということでした
はじめに言った、「コードで記述してカスタムすることができる型定義」という説明がイメージできるようになりましたでしょうか?
以上、参考になれば幸いです